You don't know js(up & going)学习笔记

第一章:走进编程

代码

语句

在计算机语言中,执行特定任务的一组单词、数字、运算符称为语句。在js中一条语句长这样:

a = b * 2;

运行程序

a = b * 2这样的语句,对于开发者的书写和阅读有好处,但不是计算机可以直接执行的形式。需要计算机上的特殊程序(一个解释器或一个编译器)将你写的代码转换成计算机可以理解的代码。

对于有些计算机语言,每次语句执行时,是自上而下逐行翻译的。这通常被称为解释器。

对于其他的语言,翻译是提前完成的,称为编译代码。所以当程序运行时,实际上是运行的已经编译好的程序。

示例

尝试在浏览器的控制台中运行以下代码(对于控制台中的换行可使用:shift + enter,运行代码则使用:enter)。

1
2
3
a = 21;
b = a * 2;
console.log( b );

输出结果:

1
42

输出

在上面代码中:

log(b):是一个函数调用,表示将b变量的值输出到控制台上。

console.:标明输出的位置为控制台。

同样我们也可以用alert(b)来输出b的值,只不过此时的b值是在页面的弹出框中显示的。

输入

在HTML中我们通常运用表单元素让用户输入值,然后用js读取输入值实现输出。但我们可以用一种更简单的方式是实现这一过程,即运用prompt(...)函数。

1
2
3
4
age = prompt('please tell me you age:');
console.log(age);
注:这段代码运行时首先弹出一个对话框提示用户输入一个值赋给age,再通过控制台输出age值。

运行结果:

值 & 类型

js中值的类型主要包含七中(第七种是ES6中新加入的):Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)、原始数据类型 (Symbol)。

类型转换

利用内置函数Number将字符串强制转换成数值:

1
2
3
4
5
6
7
var a = "42";
var b = Number( a );
console.log( a ); // "42"
console.log( b ); // 42
注:强制转换为字符串类型:String()

但是 强制转换往往会因为不熟悉其中的规则,造成很多麻烦。

代码注释

对于代码注释应该注意以下几点:

  • 没有注释的代码是不合格的代码。
  • 过多的注释(例如每行一个注释)可能是代码写的不好的标志。
  • 注释应该解释为什么,而不是这是什么。如果代码容易引起混乱,注释也能对代码做出解释。

对于代码注释,/* .. */类型的注释可以出现在语句中间,但//类型的注释则不行:

1
2
3
var a = /* arbitrary value */ 42;
console.log( a ); // 42

变量

主要介绍在js中,变量的声明不会区分值的种类,即声明一个变量后值可以为任意类型。

对于值不会发生改变的声明,变量通常用大写命名,多个单词间用_隔开,如下:

1
2
3
4
5
6
7
8
var TAX_RATE = 0.08; // 8% sales tax
var amount = 99.99;
amount = amount * 2;
amount = amount + (amount * TAX_RATE);
console.log( amount ); // 215.9784
console.log( amount.toFixed( 2 ) ); // "215.98"
注:toFixed(n)是一个函数,它将number值四舍五入n位小数,并将四舍五入后的值转换为字符型。

在ES6中,声明不能改变的常量通常使用const而不用var,如下:

1
2
3
4
// as of ES6:
const TAX_RATE = 0.08;
var amount = 99.99;

不能改变其值的常量就像变量一样有用,并且常量防止了在其初始化赋值之后,在其他某处进行意外的修改。如果你试图在第一次声明常量TAX_RATE之后,又给它赋予一个不同的值,你的程序会拒绝这次改变。

在JavaScript中,块即是定义在一对花括号{ .. }中的一条或多条语句。

条件语句

主要讲的就是if语句的使用

循环

主要讲的就是do...while, while, for循环。

函数

主要讲的就是函数,但其中有一个点需要注意,js中可以直接调用全局变量中的一个参数而不需要传参。如下:

1
2
3
4
5
6
7
8
function printAmount() {
console.log( amount.toFixed( 2 ) );
}
var amount = 99.99;
printAmount(); // "99.99"
amount = amount * 2;
printAmount(); // "199.98"

作用域

总的概括就是说,作用域内层可以访问位于它外层的任意变量。

小试牛刀

题目

  • 编写一个程序去计算手机的总价格,当你银行账户里面的钱用光的时候,你才能停止购买。你也可以为你的手机购买一些配件,只要价格在你的心理承受价格之下。
  • 计算出购买金额后,添加税款,然后用正确格式打印出实际购买金额。
  • 最后,将总价与你银行上的余额作比较,来看看你是否支付得起。
  • 你应当为“税率”,“手机价格”,“配件价格”和“花费预算”设置一些常数,也为你的“银行账户余额”设置一个变量。
  • 你应当定义函数为税费的计算和价格的格式化 —— 使用一个“$”并四舍五入到小数点后两位 。
  • 加分挑战: 试着在这个程序中使用输入,也许是使用在前面的“输入”中讲过的prompt(..)。比如,你可能会提示用户输入它们的银行账户余额。

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const TAX_RATE = 0.08;
const PHONE_PRICE = 2000;
const ACCESSORY_PRICE = 200;
const SPENDING = 5000;
var amount = 0;
function afterTax (amount) {
var price = amount * (1 + TAX_RATE);
return price.toFixed(2);
}
while (SPENDING - amount > 2000) {
amount = amount + PHONE_PRICE;
if(SPENDING - amount > ACCESSORY_PRICE) {
amount += ACCESSORY_PRICE;
}
}
afterTax = afterTax (amount);
console.log('购买商品税后总价格为:' + '$' + afterTax);
if (afterTax < SPENDING) {
console.log('支付得起!');
} else {
console.log('支付不起!');
}

复习

学习编程不一定是个复杂而且巨大的过程。你只需要在脑中装进几个基本的概念。

它们就像构建块儿。要建一座高塔,你就要从堆砌构建块儿开始。编程也一样。这里是一些编程中必不可少的构建块儿:

  • 你需要 操作符 来在值上实施动作。
  • 你需要值和 类型 来试试不同种类的动作,比如在number上做数学,或者使用string输出。
  • 你需要 变量 在你程序执行的过程中存储数据(也就是 状态)。
  • 你需要 条件,比如if语句来做决定。
  • 你需要 循环 来重复任务,直到一个条件不再成立。
  • 你需要 函数 来将你的代码组织为有逻辑的和可复用的块儿。

第二章:走进javaScript

本章将深入的学习javaScript。

值与类型

前面一章节讲过,javaScript中 只有值才区分类型,变量不会区分类型。 下面是可用的内置类型。

  • string
  • number
  • boolean
  • nullundefined
  • object
  • symbol (ES6新增类型)

下面介绍typof操作符,主要用来显示值的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var a;
typeof a; // "undefined"
a = "hello world";
typeof a; // "string"
a = 42;
typeof a; // "number"
a = true;
typeof a; // "boolean"
a = null;
typeof a; // "object" -- 奇怪的bug
a = undefined;
typeof a; // "undefined"
a = { b: "c" };
typeof a; // "object"

typeof返回的值是七中类型的 字符串之一。

typeof null是一个有趣的例子,因为当你期望它返回"null"时,它错误地返回了"object"

警告: 这是JS中一直存在的一个bug,但是看起来它永远都不会被修复了。在网络上有太多的代码依存于这个bug,因此修复它将会导致更多的bug!

另外,注意a = undefined。我们明确地将a设置为值undefined,但是在行为上这与一个还没有被设定值的变量没有区别,比如在这个代码段顶部的var a;。一个变量可以用好几种不同的方式得到这样的“undefined”值状态,包括没有返回值的函数和使用void操作符。

对象

对象已经是一个很熟悉的属性了:

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = {
a: "hello world",
b: 42,
c: true
};
obj.a; // "hello world"
obj.b; // 42
obj.c; // true
obj["a"]; // "hello world"
obj["b"]; // 42
obj["c"]; // true

属性访问的两种方式:

obj.a (点号标记法):更短而且一般来说更易于阅读,因此在可能的情况下它都是首选。

obj["a"](方括号标记法):名称中含有特殊字符的属性名称,如obj["hello world!"][ ]标记法要求一个变量(下一节讲解)或者一个string 字面量(它需要包装进" .. "' .. ')。如果你想访问一个属性/键,但是它的名称被存储在另一个变量中时,方括号标记法也很有用。例如:

1
2
3
4
5
6
7
8
9
var obj = {
a: "hello world",
b: 42
};
var b = "a";
obj[b]; // "hello world"
obj["b"]; // 42

数组

数组的使用:

1
2
3
4
5
6
7
8
9
10
11
12
var arr = [
"hello world",
42,
true
];
arr[0]; // "hello world"
arr[1]; // 42
arr[2]; // true
arr.length; // 3
typeof arr; // "object"

数组是一种特殊的对象(正如typeof所暗示的),所以它们可以拥有属性,包括一个可以自动被更新的length属性。

函数

1
2
3
4
5
6
7
8
9
function foo() {
return 42;
}
foo.bar = "hello world";
typeof foo; // "function"
typeof foo(); // "number"
typeof foo.bar; // "string"

同样地,函数也是object的子类型。

内置类型的方法

我们刚刚讨论的内建类型和子类型拥有十分强大和有用的行为,它们作为属性和方法暴露出来。

例如:

1
2
3
4
5
6
var a = "hello world";
var b = 3.14159;
a.length; // 11
a.toUpperCase(); // "HELLO WORLD"
b.toFixed(4); // "3.1416"

值的比较

强制转换

  • 明确强制转换:
1
2
3
4
5
6
var a = "42";
var b = Number( a ); //利用Number进行转换
a; // "42"
b; // 42 -- 数字!
  • 隐形强制转换:
1
2
3
4
5
6
var a = "42";
var b = a * 1; // 这里 "42" 被隐含地强制转换为 42
a; // "42"
b; // 42 -- 数字!

Truthy & Falsy

在JavaScript中“falsy”的明确列表如下:

  • "" (空字符串)
  • 0, -0, NaN (非法的number
  • null, undefined
  • false

任何不在这个“falsy”列表中的值都是“truthy”。这是其中的一些例子:

  • "hello"
  • 42
  • true
  • [ ], [ 1, "2", 3 ] (数组)
  • { }, { a: 42 } (对象)
  • function foo() { .. } (函数)

等价性

四种等价性操作符:=====!=,和!==

=====之间的不同:==在允许强制转换的条件下检查值的等价性,而===是在不允许强制转换的条件下检查值的等价性;因此===常被称为“严格等价”。

1
2
3
4
5
var a = "42";
var b = 42;
a == b; // true
a === b; // false

在各种情况下判断是否使用=====的简单规则:

  • 如果进行比较的两个值之一可能是truefalse值,使用===
  • 如果进行比较的两个值之一可能是这些具体的值(0"",或[] —— 空数组),使用===
  • 所有 其他情况下,使用==是安全的。它不仅安全,而且在许多情况下它可以简化你的代码并改善可读性。

总的来说就是, 如果你可以确定这些值,那么使用==就是安全的,如果你不能确定这些值,就使用===

特殊:两个内容相同的数组他们是不相等的。array默认情况下会通过使用逗号(,)连接所有值来被强制转换为string

1
2
3
4
5
6
7
8
9
var a = [1,2,3];
var b = [1,2,3];
var c = "1,2,3";
a == c; // true
b == c; // true
a == b; // false
a === b; // false

不等价性

<><=,和>=操作符用于不等价性比较,在语言规范中被称为“关系比较”。

既可以让number进行比较(2 < 3 ),也可以让string进行比较。当string进行比较时,它使用典型的字母顺序规则("bar" < "foo")。

不等价性的强制性转换和==一样。没有像===严格等价操作符那样不允许强制转换的“严格不等价”操作符。

1
2
3
4
5
6
var a = 41;
var b = "42";
var c = "43";
a < b; // true
b < c; // true

当两个值之一不是string,就像a < b,那么两个值就将被强制转换成number,并进行一般的数字比较。

同样,需要注意,数字和字符串之间无法进行比较。

1
2
3
4
5
6
var a = 42;
var b = "foo";
a < b; // false
a > b; // false
a == b; // false

<>的比较中,值b被强制转换为了“非法的数字值”,而且语言规范说 NaN既不大于其他值,也不小于其他值

==的比较中,如果a == b被解释为42 == NaN或者"42" == "foo"都会失败 。

变量

一个标识符必须以a-zA-Z$,或_开头。它可以包含任意这些字符外加数字0-9。并且不能包含保留字。

函数作用域

使用var关键字声明的变量将属于当前的函数作用域,如果声明位于任何函数外部的顶层,它就属于全局作用域。

提升

无论var出现在一个作用域内部的何处,这个声明都被认为是属于整个作用域,而且在作用域的所有位置都是可以访问的。这种行为成为提升

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 2;
foo(); // 可以工作, 因为 `foo()` 声明被“提升”了
function foo() {
a = 3;
console.log( a ); // 3
var a; // 声明被“提升”到了 `foo()` 的顶端
}
console.log( a ); // 2

警告: var声明的变量提升是不提倡的。而使用被提升的函数声明很常见,也更为人所接受,就像我们在foo()正式声明之前就调用它一样。

嵌套的作用域

var声明的变量,在作用域的本层和内层都是可用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function foo() {
var a = 1;
function bar() {
var b = 2;
function baz() {
var c = 3;
console.log( a, b, c ); // 1 2 3
}
baz();
console.log( a, b ); // 1 2
}
bar();
console.log( a ); // 1
}
foo();

ES6允许使用let关键字声明属于个别块儿的(一个{...} )变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo() {
var a = 1;
if (a >= 1) {
let b = 2;
while (b < 5) {
let c = b * 2;
b++;
console.log( a + c );
}
}
}
foo();
// 5 7 9

因为使用了let而非varb将仅属于if语句而不是整个foo()函数的作用域。相似地,c仅属于while循环。对于以更加细粒度的方式管理你的变量作用域来说,块儿作用域是非常有用的,它将使你的代码随着时间的推移更加易于维护。

条件

switch语句的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (a) {
case 2:
// 做一些事情
break;
case 10:
// 做另一些事请
break;
case 42:
// 又是另外一些事情
break;
default:
// 这里是备用方案
}

注意break的使用。

Strict模式*

ES5在语言中加入了一个“strict模式”,使代码更加规范、更安全、更符合逻辑、使代码对引擎有更强的可优化性。strict模式对代码有很大的好处,应当在你所有的程序中使用它。

根据摆放strict模式注解的位置,可以为一个单独的函数,或者是整个一个文件切换到strict模式:

1
2
3
4
5
6
7
8
9
10
11
function foo() {
"use strict";
// 这部分代码是strict模式的
function bar() {
// 这部分代码是strict模式的
}
}
// 这部分代码不是strict模式的
1
2
3
4
5
6
7
8
9
10
11
"use strict";
function foo() {
// 这部分代码是strict模式的
function bar() {
// 这部分代码是strict模式的
}
}
// 这部分代码是strict模式的

函数作为值

1
2
3
4
5
6
7
var foo = function() {
return 20;
};
var x = function bar(){
// ..
};

第一种:匿名函数表达式。

第二种:函数命名为bar。

立即被调用的函数表达式(IIFE)

上面的代码中,当执行foo()时,控制台里面输出的是function () {...}。函数表达式并没有执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var test = function () {
return 20;
}
test; // function () {...}
var foo = (function () {
return 20;
})();
foo; // 20
(function test () {
console.log(20); // 20
})();

简单的理解为IIFE是以下代码的缩写:

1
2
3
4
5
function test () {
console.log(20);
}
test(); // 20

闭包(closure)*

闭包是js中很重要的一部分。

你可以认为闭包是这样一种方法:即使函数已经完成了运行,它依然可以“记住”并持续访问函数的作用域。

1
2
3
4
5
6
7
8
9
10
function makeAdder(x) {
// 参数 `x` 是一个内部变量
// 内部函数 `add()` 使用 `x`,所以它对 `x` 拥有一个“闭包”
function add(y) {
return y + x;
};
return add;
}

每次调用makAdder(...)返回的内部函数add{...},可以记住makAdder(...)调用传入的x值。

对这个函数进行调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// `plusOne` 得到一个指向内部函数 `add(..)` 的引用,
// `add()` 函数拥有对外部 `makeAdder(..)` 的参数 `x`
// 的闭包
var plusOne = makeAdder( 1 );
// `plusTen` 得到一个指向内部函数 `add(..)` 的引用,
// `add()` 函数拥有对外部 `makeAdder(..)` 的参数 `x`
// 的闭包
var plusTen = makeAdder( 10 );
plusOne( 3 ); // 4 <-- 1 + 3
plusOne( 41 ); // 42 <-- 1 + 41
plusTen( 13 ); // 23 <-- 10 + 13

模块

在JavaScript中闭包最常见的用法就是模块模式。模块让你定义对外面世界不可见的私有实现细节(变量,函数),和对外面可访问的公有API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function User(){
var username, password;
function doLogin(user,pw) {
username = user;
password = pw;
console.log(username + ' ' + password);
// 做登录的工作
}
var publicAPI = {
login: doLogin
};
return publicAPI;
}
// 创建一个 `User` 模块的实例
var fred = User();
fred.login( "fred", "12Battery34!" ); // fred 12Battery34!

函数User()作为一个外部作用域持有变量usernamepassword,以及内部doLogin()函数;它们都是User模块内部的私有细节,是不能从外部世界访问的。

警告: 我们在这里没有调用new User(),这是有意为之的,虽然对大多数读者来说那可能更常见。User()只是一个函数,不是一个要被初始化的对象,所以它只是被一般地调用了。使用new将是不合适的,而且实际上会浪费资源。

在这段代码中,内部的doLogin()函数在usernamepassword上拥有闭包,这意味着即便User()函数已经完成了运行,它依然持有对它们的访问权。

this标识符

本节主要讨论this标识符的具体指向。

如果一个函数在它内部拥有一个this引用,那么这个this引用通常指向一个object。但是指向哪一个object要看这个函数是如何被调用的。重要的是要理解this 不是 指函数本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function foo() {
console.log( this.bar );
}
var bar = "global";
var obj1 = {
bar: "obj1",
foo: foo
};
var obj2 = {
bar: "obj2"
};
// --------
foo(); // "global"
obj1.foo(); // "obj1"
foo.call( obj2 ); // "obj2"
new foo(); // undefined

关于this如何被设置有四个规则,它们被展示在这个代码段的最后四行中:

  1. foo()最终在非strict模式中将this设置为全局对象 —— 在strict模式中,this将会是undefined而且你会在访问bar属性时得到一个错误 —— 所以this.bar的值是global
  2. obj1.foo()this设置为对象obj1
  3. foo.call(obj2)this设置为对象obj2
  4. new foo()this设置为一个新的空对象。

原型(Prototypes )

从一个对象到它备用对象的内部原型引用链接发生在这个对象被创建的时候。说明它的最简单的方法是使用称为Object.create(..)的内建工具。

1
2
3
4
5
6
7
8
9
10
11
var foo = {
a: 42
};
// 创建 `bar` 并将它链接到 `foo`
var bar = Object.create( foo );
bar.b = "hello world";
bar.b; // "hello world"
bar.a; // 42 <-- 委托到 `foo`

js中的原型可以类比java中的类的继承,上述代码中的foo就相当于父类,bar = Object.create( foo )就是java中的bar子类继承foo父类。

旧的与新的

为了解决浏览器对js版本的兼容性。有两种主要的技术可以将新的JavaScript特性“带到”老版本的浏览器中:填补和转译。

填补

例如,ES6定义了一个称为Number.isNaN(..)的工具,来为检查NaN值提供一种准确无误的方法,同时废弃原来的isNaN(..)工具。

1
2
3
4
5
if (!Number.isNaN) {
Number.isNaN = function isNaN(x) {
return x !== x;
};
}

if语句决定着在这个工具已经存在的ES6环境中不再进行填补。如果它还不存在,我们就定义Number.isNaN(..)

并不是所有的新特性都可以完全填补。

转译

这是一个转译的简单例子。ES6增加了一个称为“默认参数值”的新特性。它看起来像是这样:

1
2
3
4
5
6
function foo(a = 2) {
console.log( a );
}
foo(); // 2
foo( 42 ); // 42

简单,对吧?也很有用!但是这种新语法在前ES6引擎中是不合法的。那么转译器将会对这段代码做什么才能使它在老版本环境中运行呢?

1
2
3
4
function foo() {
var a = arguments[0] !== (void 0) ? arguments[0] : 2;
console.log( a );
}

如你所见,它检查arguments[0]值是否是void 0(也就是undefined),而且如果是,就提供默认值2;否则,它就赋值被传递的任何东西。

非javaScript

即我们经常使用的DOM API等。